package xiongwei.jiang.service.impl;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import xiongwei.jiang.constant.WXWorkApiConstants;
import xiongwei.jiang.constant.WXWorkConstants;
import xiongwei.jiang.constant.enums.CheckinType;
import xiongwei.jiang.constant.enums.State;
import xiongwei.jiang.constant.enums.UploadTempType;
import xiongwei.jiang.constant.enums.WXWorkApiType;
import xiongwei.jiang.exception.WXWorkException;
import xiongwei.jiang.model.bo.Client;
import xiongwei.jiang.model.dto.request.*;
import xiongwei.jiang.model.dto.response.*;
import xiongwei.jiang.prop.WXWorkProperties;
import xiongwei.jiang.service.EhcacheService;
import xiongwei.jiang.service.WXWorkService;
import xiongwei.jiang.util.MathUtils;
import xiongwei.jiang.util.XmlUtils;
import xiongwei.jiang.wxwork.WXBizMsgCrypt;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 企业微信服务接口实现类
 *
 * @author 天之蓝
 */
@EnableConfigurationProperties(value = WXWorkProperties.class)
@Service
public class WXWorkServiceImpl implements WXWorkService, InitializingBean {
    private static final Logger LOGGER = LoggerFactory.getLogger(WXWorkServiceImpl.class);

    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @Resource
    private EhcacheService ehcacheService;

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private WXWorkProperties wxWorkProperties;

    @Override
    public void afterPropertiesSet() {
        if (Objects.isNull(wxWorkProperties.getCheckin())
                && Objects.isNull(wxWorkProperties.getContact())
                && Objects.isNull(wxWorkProperties.getApp())
                && Objects.isNull(wxWorkProperties.getCallback())
                && Objects.isNull(wxWorkProperties.getBot())) {
            throw new WXWorkException("wxwork 初始化异常：缺少重要参数（参数配置请参考 README.md，项目地址：https://gitee.com/xiongwei-jiang/wxwork-spring-boot-starter/）");
        }
    }

    @SneakyThrows
    @Override
    public String getToken(WXWorkApiType type, String name, String corpId, String secret) {
        String url = String.format(WXWorkApiConstants.GET_TOKEN_URL, corpId, secret);
        return OBJECT_MAPPER.readValue(this.getAuth(type, name, URI.create(url)), Token.class).getAccess_token();
    }

    @Override
    public String getTokenCache(WXWorkApiType type, String name, String corpId, String secret) {
        String cache = ehcacheService.getStringCache(WXWorkConstants.WXWORK_CACHE_ALIAS, String.format(WXWorkConstants.TOKEN_CACHE_KEY, type, name));
        if (Objects.isNull(cache)) {
            cache = this.getToken(type, name, corpId, secret);
            ehcacheService.setStringCache(WXWorkConstants.WXWORK_CACHE_ALIAS, String.format(WXWorkConstants.TOKEN_CACHE_KEY, type, name), cache, WXWorkConstants.WXWORK_CACHE_TTL);
        }
        return cache;
    }

    @SneakyThrows
    @Override
    public void verifyUrl(String name, String msg_signature, String timestamp, String nonce, String echostr, HttpServletResponse response) {
        WXWorkProperties.Callback prop = wxWorkProperties.getCallback().get(name);
        String data = new WXBizMsgCrypt(prop.getToken(),
                prop.getEncodingAesKey(),
                prop.getCorpId()).VerifyURL(msg_signature, timestamp, nonce, echostr);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("[{} {}] recv GET 明文 {}", WXWorkApiType.CALLBACK, name, data);
        }
        response.getWriter().print(data);
    }

    @SneakyThrows
    @Override
    public Map<String, Object> callback(String name, String msg_signature, String timestamp, String nonce, String xml, HttpServletResponse response) {
        WXWorkProperties.Callback prop = wxWorkProperties.getCallback().get(name);
        String data = new WXBizMsgCrypt(prop.getToken(),
                prop.getEncodingAesKey(),
                prop.getCorpId()).DecryptMsg(msg_signature, timestamp, nonce, xml);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("[{} {}] recv POST 明文 {}", WXWorkApiType.CALLBACK, name, data);
        }
        response.getWriter().print("success");
        return XmlUtils.xml2Map(data);
    }

    @SneakyThrows
    @Override
    public UserInfo getUserInfo(WXWorkApiType type, String name, String userId) {
        Client client = this.getClient(type, name);
        String url = String.format(WXWorkApiConstants.GET_USER_INFO_URL, this.getTokenCache(type, name, client.getId(), client.getSecret()), userId);
        return OBJECT_MAPPER.readValue(this.get(type, name, URI.create(url)), UserInfo.class);
    }

    @SneakyThrows
    @Override
    public DepartmentUser getDepartmentUser(WXWorkApiType type, String name, int departmentId, State fetchChild) {
        Client client = this.getClient(type, name);
        String url = String.format(WXWorkApiConstants.GET_DEPARTMENT_USER_URL, this.getTokenCache(type, name, client.getId(), client.getSecret()), departmentId, fetchChild.getCode());
        return OBJECT_MAPPER.readValue(this.get(type, name, URI.create(url)), DepartmentUser.class);
    }

    @SneakyThrows
    @Override
    public DepartmentList getDepartmentList(WXWorkApiType type, String name, int departmentId) {
        Client client = this.getClient(type, name);
        String url = String.format(WXWorkApiConstants.GET_DEPARTMENT_LIST_URL, this.getTokenCache(type, name, client.getId(), client.getSecret()), departmentId);
        return OBJECT_MAPPER.readValue(this.get(type, name, URI.create(url)), DepartmentList.class);
    }

    @SneakyThrows
    @Override
    public TagUser getTagUser(WXWorkApiType type, String name, int tagId) {
        Client client = this.getClient(type, name);
        String url = String.format(WXWorkApiConstants.GET_TAG_USER_URL, this.getTokenCache(type, name, client.getId(), client.getSecret()), tagId);
        return OBJECT_MAPPER.readValue(this.get(type, name, URI.create(url)), TagUser.class);
    }

    @SneakyThrows
    @Override
    public TagList getTagList(WXWorkApiType type, String name) {
        Client client = this.getClient(type, name);
        String url = String.format(WXWorkApiConstants.GET_TAG_LIST_URL, this.getTokenCache(type, name, client.getId(), client.getSecret()));
        return OBJECT_MAPPER.readValue(this.get(type, name, URI.create(url)), TagList.class);
    }

    @SneakyThrows
    @Override
    public User getUser(String name, String code) {
        WXWorkProperties.App prop = wxWorkProperties.getApp().get(name);
        String url = String.format(WXWorkApiConstants.GET_USER_URL, this.getTokenCache(WXWorkApiType.APP, name, prop.getCorpId(), prop.getSecret()), code);
        return OBJECT_MAPPER.readValue(this.get(WXWorkApiType.APP, name, URI.create(url)), User.class);
    }

    @Override
    public AppMsg sendAppTextMsg(String name, TextAppMsg object) {
        return this.sendAppMsg(name, object);
    }

    @Override
    public AppMsg sendAppMarkdownMsg(String name, MarkdownAppMsg object) {
        return this.sendAppMsg(name, object);
    }

    @SneakyThrows
    @Override
    public UploadTemp uploadTemp(String name, UploadTempType tempType, String filePath) {
        WXWorkProperties.App prop = wxWorkProperties.getApp().get(name);
        String url = String.format(WXWorkApiConstants.POST_UPLOAD_TEMP_URL, this.getTokenCache(WXWorkApiType.APP, name, prop.getCorpId(), prop.getSecret()), tempType.getCode());
        return OBJECT_MAPPER.readValue(this.post(WXWorkApiType.APP, name, URI.create(url), filePath), UploadTemp.class);
    }

    @SneakyThrows
    @Override
    public UploadImage uploadImage(String name, String filePath) {
        WXWorkProperties.App prop = wxWorkProperties.getApp().get(name);
        String url = String.format(WXWorkApiConstants.POST_UPLOAD_IMAGE_URL, this.getTokenCache(WXWorkApiType.APP, name, prop.getCorpId(), prop.getSecret()));
        return OBJECT_MAPPER.readValue(this.post(WXWorkApiType.APP, name, URI.create(url), filePath), UploadImage.class);
    }

    @Override
    public byte[] downloadTemp(String name, String mediaId) {
        WXWorkProperties.App prop = wxWorkProperties.getApp().get(name);
        String url = String.format(WXWorkApiConstants.GET_DOWNLOAD_TEMP_URL, this.getTokenCache(WXWorkApiType.APP, name, prop.getCorpId(), prop.getSecret()), mediaId);
        return post(WXWorkApiType.APP, name, URI.create(url));
    }

    @Override
    public byte[] downloadVoice(String name, String mediaId) {
        WXWorkProperties.App prop = wxWorkProperties.getApp().get(name);
        String url = String.format(WXWorkApiConstants.GET_DOWNLOAD_VOICE_URL, this.getTokenCache(WXWorkApiType.APP, name, prop.getCorpId(), prop.getSecret()), mediaId);
        return post(WXWorkApiType.APP, name, URI.create(url));
    }

    @SneakyThrows
    @Override
    public CheckinRecord getCheckinRecord(String name, CheckinType openCheckinDataType, long startTime, long endTime, List<String> userIdList) {
        LinkedHashMap<String, Object> map = new LinkedHashMap<>(5);
        map.put("opencheckindatatype", openCheckinDataType.getCode());
        map.put("starttime", startTime);
        map.put("endtime", endTime);
        map.put("useridlist", userIdList);
        WXWorkProperties.Checkin prop = wxWorkProperties.getCheckin().get(name);
        String url = String.format(WXWorkApiConstants.POST_CHECKIN_RECORD_URL, this.getTokenCache(WXWorkApiType.CHECKIN, name, prop.getCorpId(), prop.getSecret()));
        return OBJECT_MAPPER.readValue(this.post(WXWorkApiType.CHECKIN, name, URI.create(url), map), CheckinRecord.class);
    }

    @SneakyThrows
    @Override
    public String getCorpTicket(String name) {
        WXWorkProperties.App prop = wxWorkProperties.getApp().get(name);
        String url = String.format(WXWorkApiConstants.GET_CORP_TICKET_URL, this.getTokenCache(WXWorkApiType.APP, name, prop.getCorpId(), prop.getSecret()));
        return OBJECT_MAPPER.readValue(this.getAuth(WXWorkApiType.APP, name, URI.create(url)), Ticket.class).getTicket();
    }

    @Override
    public String getCorpTicketCache(String name) {
        String cache = ehcacheService.getStringCache(WXWorkConstants.WXWORK_CACHE_ALIAS, String.format(WXWorkConstants.CORP_TICKET_CACHE_KEY, WXWorkApiType.APP, name));
        if (cache == null) {
            cache = this.getCorpTicket(name);
            ehcacheService.setStringCache(WXWorkConstants.WXWORK_CACHE_ALIAS, String.format(WXWorkConstants.CORP_TICKET_CACHE_KEY, WXWorkApiType.APP, name), cache, WXWorkConstants.WXWORK_CACHE_TTL);
        }
        return cache;
    }

    @SneakyThrows
    @Override
    public String getAppTicket(String name) {
        WXWorkProperties.App prop = wxWorkProperties.getApp().get(name);
        String url = String.format(WXWorkApiConstants.GET_APP_TICKET_URL, this.getTokenCache(WXWorkApiType.APP, name, prop.getCorpId(), prop.getSecret()));
        return OBJECT_MAPPER.readValue(this.getAuth(WXWorkApiType.APP, name, URI.create(url)), Ticket.class).getTicket();
    }

    @Override
    public String getAppTicketCache(String name) {
        String cache = ehcacheService.getStringCache(WXWorkConstants.WXWORK_CACHE_ALIAS, String.format(WXWorkConstants.APP_TICKET_CACHE_KEY, WXWorkApiType.APP, name));
        if (cache == null) {
            cache = this.getAppTicket(name);
            ehcacheService.setStringCache(WXWorkConstants.WXWORK_CACHE_ALIAS, String.format(WXWorkConstants.APP_TICKET_CACHE_KEY, WXWorkApiType.APP, name), cache, WXWorkConstants.WXWORK_CACHE_TTL);
        }
        return cache;
    }

    @Override
    public Map<String, Object> getCorpConfig(String name, String webUrl) {
        Map<String, Object> config = new HashMap<>();
        config.put("appId", wxWorkProperties.getApp().get(name).getAgentId());
        String ticket = this.getCorpTicketCache(name);
        this.fillConfig(ticket, webUrl, config);
        return config;
    }

    @Override
    public Map<String, Object> getAppConfig(String name, String webUrl) {
        WXWorkProperties.App prop = wxWorkProperties.getApp().get(name);
        Map<String, Object> config = new HashMap<>();
        config.put("corpid", prop.getCorpId());
        config.put("agentid", prop.getAgentId());
        String ticket = this.getAppTicketCache(name);
        this.fillConfig(ticket, webUrl, config);
        return config;
    }

    @SneakyThrows
    @Override
    public Result sendBotTextMsg(String name, TextBotMsg object) {
        URI url = wxWorkProperties.getBot().get(name).getWebhookUrl();
        return OBJECT_MAPPER.readValue(this.post(WXWorkApiType.BOT, name, url, object), Result.class);
    }

    @SneakyThrows
    @Override
    public Result sendBotMarkdownMsg(String name, MarkdownBotMsg object) {
        URI url = wxWorkProperties.getBot().get(name).getWebhookUrl();
        return OBJECT_MAPPER.readValue(this.post(WXWorkApiType.BOT, name, url, object), Result.class);
    }

    private Client getClient(WXWorkApiType type, String name) {
        String id;
        String secret;
        switch (type) {
            case CONTACT:
                WXWorkProperties.Contact contact = wxWorkProperties.getContact().get(name);
                id = contact.getCorpId();
                secret = contact.getSecret();
                break;
            case APP:
                WXWorkProperties.App app = wxWorkProperties.getApp().get(name);
                id = app.getCorpId();
                secret = app.getSecret();
                break;
            default:
                throw new WXWorkException(WXWorkConstants.UNSUPPORTED_API_TYPE_ERROR + type);
        }
        return new Client(id, secret);
    }

    @SneakyThrows
    private AppMsg sendAppMsg(String name, AppMsgBody object) {
        WXWorkProperties.App prop = wxWorkProperties.getApp().get(name);
        object.setAgentid(prop.getAgentId());
        String url = String.format(WXWorkApiConstants.POST_SEND_APP_MSG_URL, this.getTokenCache(WXWorkApiType.APP, name, prop.getCorpId(), prop.getSecret()));
        return OBJECT_MAPPER.readValue(this.post(WXWorkApiType.APP, name, URI.create(url), object), AppMsg.class);
    }

    public String get(WXWorkApiType type, String name, URI url) {
        ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, String.class);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("[{} {}] send GET 请求 {} 响应 {}", type, name, url.getPath(), responseEntity.getBody());
        }
        return responseEntity.getBody();
    }

    @SneakyThrows
    public String getAuth(WXWorkApiType type, String name, URI url) {
        ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, String.class);
        if (OBJECT_MAPPER.readTree(responseEntity.getBody()).get("errcode").asInt() != 0) {
            throw new WXWorkException(String.format("[%s %s] send GET 请求 %s 响应 %s", type, name, url.getPath(), responseEntity.getBody()));
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("[{} {}] send GET 请求 {} 响应 {}", type, name, url.getPath(), responseEntity.getBody());
        }
        return responseEntity.getBody();
    }

    public String post(WXWorkApiType type, String name, URI url, Object body) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<Object> httpEntity = new HttpEntity<>(body, httpHeaders);
        ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, String.class);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("[{} {}] send POST 请求 {} 响应 {}", type, name, url.getPath(), responseEntity.getBody());
        }
        return responseEntity.getBody();
    }

    @SneakyThrows
    public String post(WXWorkApiType type, String name, URI url, String filePath) {
        MultiValueMap<String, Object> form = new LinkedMultiValueMap<>(2);
        FileSystemResource fileSystemResource = new FileSystemResource(filePath);
        form.add("file", fileSystemResource);
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
        httpHeaders.setContentLength(fileSystemResource.contentLength());
        httpHeaders.setContentDispositionFormData("media", fileSystemResource.getFilename());
        HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(form, httpHeaders);
        ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, String.class);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("[{} {}] send POST 请求 {} 响应 {}", type, name, url.getPath(), responseEntity.getBody());
        }
        return responseEntity.getBody();
    }

    public byte[] post(WXWorkApiType type, String name, URI url) {
        HttpEntity<byte[]> responseEntity = restTemplate.exchange(url, HttpMethod.POST, HttpEntity.EMPTY, byte[].class);
        if (Objects.requireNonNull(responseEntity.getHeaders().getContentType()).getSubtype().equalsIgnoreCase("json")) {
            throw new WXWorkException(String.format("[%s %s] send POST 请求 %s 响应 %s", type, name, url.getPath(), new String(Objects.requireNonNull(responseEntity.getBody()), StandardCharsets.UTF_8)));
        }
        return responseEntity.getBody();
    }

    private void fillConfig(String ticket, String webUrl, Map<String, Object> config) {
        long timestamp = System.currentTimeMillis() / 1000;
        config.put("timestamp", timestamp);
        String nonceStr = UUID.randomUUID().toString();
        config.put("nonceStr", nonceStr);
        webUrl = webUrl.split("#")[0];
        int index = webUrl.length() - 1;
        if (webUrl.lastIndexOf("/") == index) {
            webUrl = webUrl.substring(0, index);
        }
        String signature = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "&timestamp=" + timestamp + "&url=" + webUrl;
        signature = MathUtils.sha1Hex(signature);
        config.put("signature", signature);
        config.put("jsApiList", "");
    }
}
